iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Modern Web

重新認識Vue 走過路過不要錯過系列 第 7

Vue 走過路過不要錯過 Day07 - watch 與 watchEffect:什麼時候該用哪一個

  • 分享至 

  • xImage
  •  

前言

資料變了,我們常常需要「順便做點什麼」:打 API、存進 localStorage、印個 log。Vue 3 給了我們 computedwatchwatchEffect 三個看起來都跟「資料變化」有關的工具,於是問題就來了:到底該用哪一個?

先講結論:

需要一個值,用 computed;需要做一件事,用 watch 系列。要明確指定來源、拿到舊值、或不想一開始就執行,用 watch;只想「用到什麼就追蹤什麼」並且立刻執行,用 watchEffect。

接下來分成三個部分說明:先排除 computed,再從三個角度比較 watch 與 watchEffect,最後整理成一張決策表。


先排除:你真的需要 watch 嗎?

很多人在問「watch 還是 watchEffect」之前,其實該先問「我需不需要 watch」。分界只有一句話:

你需要的是「一個值」→ computed;你需要的是「做一件事」→ watch。

computed 是從既有資料推導出新的值,會回傳結果、有快取,依賴沒變就不重算,應該保持純粹。watch 則是資料變化時去執行副作用,本身不產生值給畫面用。

最常見的誤用,是拿 watch 去「同步」一個其實算得出來的值:

// 不建議:用 watch 手動同步衍生值
const firstName = ref('承')
const lastName = ref('哲')
const fullName = ref('')
 
watch([firstName, lastName], ([f, l]) => {
  fullName.value = f + l
}, { immediate: true })
 
// 建議:它本來就是推導出來的值
const fullName = computed(() => firstName.value + lastName.value)

判斷訊號:如果 watch 的 callback 裡唯一做的事是「把結果塞給另一個 ref」,那它八成該是 computed。

確認自己是要「做事」之後,才進入 watch 與 watchEffect 的選擇。


比較一:依賴怎麼決定

watch:你明確告訴它要看誰

watch 的第一個參數叫 source(來源),可以是以下幾種:

const count = ref(0)
const state = reactive({ keyword: '', page: 1 })
 
watch(count, (newVal, oldVal) => {})              // 一個 ref
watch(state, (newVal, oldVal) => {})              // 一個 reactive 物件
watch(() => state.keyword, (newVal, oldVal) => {}) // getter 函式
watch([count, () => state.page], ([c, p]) => {})  // 陣列,同時看多個

這裡有個新手很常踩的坑:想監聽 reactive 物件裡的某個屬性,直接寫成這樣是無效的:

watch(state.keyword, () => {}) // ❌ 傳進去的只是一個字串,不是響應式來源
watch(() => state.keyword, () => {}) // ✅ 用 getter 包起來

state.keyword 在傳進 watch 的那一刻就已經被取值成普通字串了,Vue 沒辦法從一個字串知道它該監聽什麼,所以要用 getter 函式,讓 Vue 在執行 getter 時去收集依賴。

watchEffect:用到什麼,就追蹤什麼

watchEffect 不需要指定來源:

const userId = ref(1)
 
watchEffect(() => {
  fetchUser(userId.value)
})

它怎麼知道要在 userId 變化時重新執行?答案就在 Day 3 做過的迷你 reactive 裡:Proxy 的 get 被觸發時,會記錄「現在是誰在讀我」。

watchEffect 做的事,概念上就是把自己登記成「目前正在執行的 effect」,然後立刻跑一次你的函式。函式裡讀到 userId.value,觸發了 get,get 就把這個 effect 記進 userId 的依賴清單。之後 userId 被修改、觸發 set,就會通知清單裡的 effect 重新執行。

// 概念示意,非 Vue 原始碼
let activeEffect = null
 
function watchEffect(fn) {
  const effect = () => {
    activeEffect = effect
    fn()               // 執行期間讀到的響應式資料,都會在 get 裡被 track
    activeEffect = null
  }
  effect()
}
 
// 在 Proxy 的 get 裡:
// if (activeEffect) track(target, key, activeEffect)

所以 watchEffect 的依賴是「執行時實際讀到的東西」,而且每次重新執行都會重新收集一次。

這也帶來一個限制:只有同步執行期間讀到的資料才會被追蹤await 之後才讀的值,已經不在那次執行的追蹤範圍內了:

watchEffect(async () => {
  const res = await fetch(`/api/user/${userId.value}`) // ✅ await 之前讀到,會追蹤
  const data = await res.json()
  console.log(filter.value) // ❌ await 之後才讀,不會被追蹤
})

對照 React 的 useEffect

如果你寫過 React,watchEffect 看起來很像 useEffect:都是丟一個函式進去放副作用,也都會先執行一次。但關鍵差別正好是自動追蹤:

// React:依賴要自己列
useEffect(() => {
  fetchUser(userId)
}, [userId])
 
// Vue watch:來源要自己指定,比較接近上面這個
watch(userId, (id) => fetchUser(id))
 
// Vue watchEffect:不用列,自動追蹤
watchEffect(() => fetchUser(userId.value))

還有一個容易搞混的地方:useEffect 不寫依賴陣列時,會在每次 render 後都執行;watchEffect 沒有依賴陣列,卻只會在它讀到的響應式資料變化時重跑。兩邊「沒寫依賴」的意思剛好相反。原因在於 React 每次 render 都會重新執行整個元件函式,而 Vue 的 setup 只跑一次,更新靠的是響應式系統精準通知。


比較二:什麼時候執行

watch 預設是 lazy 的。 元件建立時 callback 不會執行,要等來源第一次變化才跑:

watch(keyword, (val) => {
  search(val) // 一開始不會執行,keyword 改變後才執行
})
 
watch(keyword, (val) => {
  search(val) // 加上 immediate,一開始就會先跑一次
}, { immediate: true })

watchEffect 則是建立當下就立刻執行一次,因為它必須先跑過一遍,才知道自己依賴了誰。


比較三:拿不拿得到舊值

watch 會把新值和舊值傳進 callback,適合需要比較前後差異的情境:

watch(page, (newPage, oldPage) => {
  console.log(`從第 ${oldPage} 頁換到第 ${newPage} 頁`)
})

但要注意一個陷阱:如果來源直接是一個 reactive 物件,Vue 會預設深層監聽,而 newValoldVal 會指向同一個物件,拿不到真正的舊值:

const state = reactive({ count: 0 })
 
watch(state, (newVal, oldVal) => {
  console.log(newVal === oldVal) // true
})

如果需要某個屬性的舊值,改用 getter 監聽那個屬性即可:

watch(() => state.count, (newCount, oldCount) => {
  console.log(oldCount, '→', newCount)
})

watchEffect 沒有舊值,它只關心「現在」的狀態。


補充:停止監聽

watch 與 watchEffect 本身都會回傳一個停止監聽的函式。在 setup 裡同步建立的 watcher 會跟著元件卸載自動停止,大多數時候不用手動處理;但如果是在非同步流程裡才建立的,就要記得自己停掉:

const stop = watchEffect(() => {
  console.log(userId.value)
})
 
// 不需要時
stop()

總結:決策流程

先問自己:我要的是一個值,還是要做一件事?

要一個值 → computed。要做一件事,再往下看:

比較維度 watch watchEffect
依賴怎麼決定 明確指定 source 執行時讀到什麼就追蹤什麼
什麼時候執行 預設 lazy,來源變化才執行(可加 immediate 建立時立刻執行一次
拿不拿得到舊值 可以(直接監聽 reactive 物件時例外) 不行
適合情境 只想對特定資料反應、需要前後比較、不想一開始就執行 依賴很多、只在乎最新狀態、一開始就要執行

一句話收尾:watch 是「我告訴你看誰」,watchEffect 是「你自己看我用了誰」。


上一篇
Vue 走過路過不要錯過 Day06 - computed:不只是「快取」這麼簡單
下一篇
Vue 走過路過不要錯過 Day08 - 改完資料,DOM 為什麼還是舊的?認識 nextTick
系列文
重新認識Vue 走過路過不要錯過9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言